/*
 * Copyright 2011 yingxinwu.g@gmail.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * 
 *   http://www.apache.org/licenses/LICENSE-2.0
 * 
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package xink.crypto;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;

import javax.crypto.Cipher;
import javax.crypto.CipherOutputStream;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

import net.htjs.mobile.nyoa.R;
import xink.vpn.Assert;
import android.content.Context;

public final class StreamCrypto {

	/*
	 * Buffer size when reading input.
	 */
	private static final int BUF_SIZE = 128;

	private static byte[] rawKey;

	/**
	 * Initialization.
	 * 
	 * @param ctx
	 *            Context
	 */
	public static void init(final Context ctx) {
		rawKey = ctx.getString(R.string.crypto_raw_key).getBytes();
		Assert.isEquals(rawKey.length, 16,
				"AES-128 requires a 16 bytes raw key");
	}

	/**
	 * Encrypt bytes from input stream, and writes to the output stream.<br>
	 * input and output stream will be closed before this method returns.
	 * 
	 * @throws IOException
	 *             when read/write file failed
	 * @throws GeneralSecurityException
	 *             when encrypt error occurs
	 */
	public static void encrypt(final InputStream in, final OutputStream out)
			throws IOException, GeneralSecurityException {
		process(Cipher.ENCRYPT_MODE, in, out);
	}

	/**
	 * Decrypt bytes from input stream, and writes to the output stream.<br>
	 * input and output stream will be closed before this method returns.
	 * 
	 * @throws IOException
	 *             when read/write file failed
	 * @throws GeneralSecurityException
	 *             when decrypt error occurs
	 */
	public static void decrypt(final InputStream in, final OutputStream out)
			throws IOException, GeneralSecurityException {
		process(Cipher.DECRYPT_MODE, in, out);
	}

	private static void process(final int mode, final InputStream in,
			final OutputStream out) throws IOException,
			GeneralSecurityException {
		CipherOutputStream co = null;

		try {
			co = doProcess(mode, in, out);
		} finally {
			in.close();

			if (co != null) {
				co.close();
			}
		}
	}

	private static CipherOutputStream doProcess(final int mode,
			final InputStream in, final OutputStream out) throws IOException,
			GeneralSecurityException {
		Cipher c = getCipher(mode);
		CipherOutputStream co = new CipherOutputStream(out, c);

		byte[] buf = new byte[BUF_SIZE];
		int len = -1;
		while ((len = in.read(buf)) > 0) {
			co.write(buf, 0, len);
		}
		return co;
	}

	private static Cipher getCipher(final int mode)
			throws GeneralSecurityException {
		SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES");
		IvParameterSpec ivps = new IvParameterSpec(rawKey);

		Cipher c = Cipher.getInstance("AES/CBC/PKCS5Padding");
		c.init(mode, skeySpec, ivps);
		return c;
	}

	private StreamCrypto() {

	}
}
